local assets =
{
    Asset("ANIM", "anim/terrarium.zip"),
    Asset("INV_IMAGE", "terrarium_cooldown"),
    Asset("INV_IMAGE", "terrarium_crimson"),
}

-------------------------------------------------------------------------------
local MAX_LIGHT_FRAME = 14
local MAX_LIGHT_RADIUS = 15

-- dframes is like dt, but for frames, not time
local function OnUpdateLight(inst, dframes)
    local done
    if inst._islighton:value() then
        local frame = inst._lightframe:value() + dframes
        done = frame >= MAX_LIGHT_FRAME
        inst._lightframe:set_local(done and MAX_LIGHT_FRAME or frame)
    else
        local frame = inst._lightframe:value() - dframes * 3
        done = frame <= 0
        inst._lightframe:set_local(done and 0 or frame)
    end

    inst.Light:SetRadius(MAX_LIGHT_RADIUS * inst._lightframe:value() / MAX_LIGHT_FRAME)

    if done then
        inst._LightTask:Cancel()
        inst._LightTask = nil
    end
end

local function OnUpdateLightColour(inst)
    inst._lighttweener = inst._lighttweener + FRAMES * 1.25
    if inst._lighttweener > TWOPI then
        inst._lighttweener = inst._lighttweener - TWOPI
    end

    local x = inst._lighttweener
    local s = .15
    local b = 0.85
    local sin = math.sin

    local red = sin(x) * s + b - s
    local green = sin(x + 2 / 3 * PI) * s + b - s
    local blue = sin(x - 2 / 3 * PI) * s + b - s

    inst.Light:SetColour(red, green, blue)
end

local function OnLightDirty(inst)
    if inst._LightTask == nil then
        inst._LightTask = inst:DoPeriodicTask(FRAMES, OnUpdateLight, nil, 1)
    end
    OnUpdateLight(inst, 0)

    if not TheNet:IsDedicated() then
        if inst._islighton:value() then
            if inst._lightcolourtask == nil then
                inst._lighttweener = 0
                inst._lightcolourtask = inst:DoPeriodicTask(FRAMES, OnUpdateLightColour)
            end
        elseif inst._lightcolourtask ~= nil then
            inst._lightcolourtask:Cancel()
            inst._lightcolourtask = nil
        end
    end
end
-------------------------------------------------------------------------------

local function hookup_witch_listeners(inst, witch)
    inst:ListenForEvent("onremove", inst.on_end_witch_fn, witch)
    inst:ListenForEvent("turnoff_terrarium", inst.on_end_witch_fn, witch)

    inst:ListenForEvent("finished_leaving", inst.on_witch_left_fn, witch)
end
-------------------------------------------------------------------------------

local function enable_dynshadow(inst)
    if inst._ShadowDelayTask ~= nil then
        inst._ShadowDelayTask:Cancel()
        inst._ShadowDelayTask = nil
    end
    inst.DynamicShadow:Enable(true)
end

local function disable_dynshadow(inst)
    if inst._ShadowDelayTask ~= nil then
        inst._ShadowDelayTask:Cancel()
        inst._ShadowDelayTask = nil
    end
    inst.DynamicShadow:Enable(false)
end

local function CanSummon(inst)
    return TheWorld.state.isnight
end

local function TurnOn(inst, is_loading)
    if inst.is_on then
        return
    end
    inst.is_on = true

    inst.components.activatable.inactive = true -- to allow turning off

    if is_loading then
        inst.AnimState:PlayAnimation("activated_idle", true)

        enable_dynshadow(inst)
    else
        inst.AnimState:PlayAnimation("activate")
        inst.AnimState:PushAnimation("activated_idle", true)

        if inst._ShadowDelayTask ~= nil then
            inst._ShadowDelayTask:Cancel()
        end
        inst._ShadowDelayTask = inst:DoTaskInTime(4 * FRAMES, enable_dynshadow)

        if CanSummon(inst) then
            local t = inst.components.timer:GetTimeLeft("summon_delay")
            if t == nil then
                inst.components.timer:StartTimer("summon_delay", TUNING.TERRARIUM_SUMMON_DELAY)
            elseif t < 1 then
                inst.components.timer:SetTimeLeft("summon_delay", 1)
            end
        end
    end

    inst.SoundEmitter:KillSound("beam")
    inst.SoundEmitter:PlaySound("terraria1/terrarium/shimmer_loop", "shimmer")
end

local function TurnLightsOn(inst)
    inst._islighton:set(true)
    OnLightDirty(inst)
    inst._TurnLightsOnTask = nil
end

local function StartSummoning(inst, is_loading)
    if is_loading or
        (inst.is_on and CanSummon(inst) and
            inst._summoning_fx == nil and
            not inst.components.timer:TimerExists("summon_delay")
        ) then
        -- Put the Terrarium itself into an untouchable state.
        inst.components.inventoryitem.canbepickedup = false
        inst.components.activatable.inactive = false

        -- Spawn the summoning beam, if we do not have one (and we shouldn't)
        if inst._summoning_fx == nil then
            inst._summoning_fx = SpawnPrefab("terrarium_fx")
            inst._summoning_fx.entity:SetParent(inst.entity)
            inst._summoning_fx.AnimState:PlayAnimation("activate_fx")
            inst._summoning_fx.AnimState:PushAnimation("activated_idle_fx", true)
        end
        -- ...including a delayed light activation
        inst._TurnLightsOnTask = inst:DoTaskInTime(7 * FRAMES, TurnLightsOn)

        enable_dynshadow(inst)

        inst.SoundEmitter:KillSound("shimmer")
        inst.SoundEmitter:PlaySound("terraria1/terrarium/beam_loop", "beam")

        -- If we're not starting this summoning sequence via OnLoad, do some extra presentation,
        -- and also queue up a boss spawn.
        if not is_loading then
            TheNet:Announce(STRINGS.WITCH_COMING)

            inst.components.timer:StartTimer("warning", TUNING.TERRARIUM_WARNING_TIME)

            inst.SoundEmitter:PlaySound("terraria1/terrarium/beam_shoot")
        end
    end
end

local DEACTIVATE_TIME = 10 * FRAMES
local function TurnOff(inst)
    if not inst.is_on then
        return
    end

    inst.is_on = false
    inst.components.activatable.inactive = true

    inst.components.timer:StopTimer("warning")

    if not inst.components.inventoryitem.canbepickedup then
        inst.components.inventoryitem.canbepickedup = true
    end

    inst.SoundEmitter:KillSound("shimmer")
    inst.SoundEmitter:KillSound("beam")

    if inst._TurnLightsOnTask ~= nil then
        inst._TurnLightsOnTask:Cancel()
        inst._TurnLightsOnTask = nil
    end
    inst._islighton:set(false)
    OnLightDirty(inst)

    if inst._summoning_fx ~= nil then
        inst._summoning_fx.AnimState:PlayAnimation("deactivate_fx")
        inst._summoning_fx:DoTaskInTime(DEACTIVATE_TIME, inst._summoning_fx.Remove)
        inst._summoning_fx = nil

        inst.SoundEmitter:PlaySound("terraria1/terrarium/beam_stop")
    end

    -- The Terrarium is in limbo when it's in an inventory or container.
    if inst:IsInLimbo() then
        inst.AnimState:PlayAnimation("idle", true)

        disable_dynshadow(inst)
    else
        inst.AnimState:PlayAnimation("deactivate")
        inst.AnimState:PushAnimation("idle", true)

        if inst._ShadowDelayTask ~= nil then
            inst._ShadowDelayTask:Cancel()
        end
        inst._ShadowDelayTask = inst:DoTaskInTime(4 * FRAMES, disable_dynshadow)
    end
end

local function resetTpTime(inst)
    if inst.tp_boss_to_door_time then
        if not inst.components.timer:TimerExists("tp_boss_to_door") then
            inst.components.timer:StartTimer("tp_boss_to_door", inst.tp_boss_to_door_time)
        else
            inst.components.timer:SetTimeLeft("tp_boss_to_door", inst.tp_boss_to_door_time)
        end
    end
end

local function OnBossFightOver(inst)
    TurnOff(inst)

    inst.components.activatable.inactive = false

    inst.AnimState:Hide("terrarium_tree")
    inst.AnimState:Hide("terrarium_tree_crimson")
    inst.components.inventoryitem:ChangeImageName("terrarium_cooldown")

    if inst.witch ~= nil then
        inst.witch:PushEvent("leave")
    end

    if not inst.components.worldsettingstimer:ActiveTimerExists("cooldown") then
        print(inst.cooldown_time)
        inst.components.worldsettingstimer:StartTimer("cooldown", inst.cooldown_time)
    end
    --成功击杀才会让这个计时器重置
    resetTpTime(inst)
end

local function OnDay_SendBossAway(inst)
    -- Return to the "on but not shooting a beam" state
    TurnOff(inst)

    -- Tell the boss to leave; it will tell us when it's done so via "finished_leaving"
    if inst.witch ~= nil then
        inst.witch:PushEvent("leave")
    end
end

local SPAWN_OFFSET = 10
local function SpawnWitch(inst)
    if AllPlayers ~= nil and #AllPlayers > 0 then
        local targeted_player = AllPlayers[math.random(#AllPlayers)]

        TheNet:Announce(subfmt(STRINGS.WITCH_TARGET,
            {
                player_name = targeted_player.name,
                witch_name = STRINGS.NAMES[string.upper(inst.witch_prefab)] or
                    "MISSING NAME"
            }))

        local angle = math.random() * TWOPI
        local player_pt = targeted_player:GetPosition()
        local spawn_offset = FindWalkableOffset(player_pt, angle, SPAWN_OFFSET, nil, false, true, nil, true, true)
            or Vector3(SPAWN_OFFSET * math.cos(angle), 0, SPAWN_OFFSET * math.sin(angle))
        local spawn_position = player_pt + spawn_offset

        if inst.witch ~= nil and inst.witch:IsInLimbo() then
            inst.witch:ReturnToScene()
            inst.witch.Transform:SetPosition(spawn_position:Get()) -- Needs to be done so the spawn fx spawn in the right place
            if inst.witch.sg ~= nil and inst.witch.sg:HasState("flyback") then
                inst.witch.sg:GoToState("flyback", targeted_player)
            else
                inst.witch:PushEvent("flyback", targeted_player)
            end
        else
            inst.witch = SpawnPrefab(inst.witch_prefab)
            inst.witch.Transform:SetPosition(spawn_position:Get()) -- Needs to be done so the spawn fx spawn in the right place
            if inst.witch.spawn_fn then
                inst.witch:spawn_fn()
            end
            if inst.witch.sg ~= nil and inst.witch.sg:HasState("arrive") then
                inst.witch.sg:GoToState("arrive", targeted_player)
            else
                inst.witch:PushEvent("arrive", targeted_player)
            end
        end
        inst.witch:PushEvent("set_spawn_target", targeted_player)

        hookup_witch_listeners(inst, inst.witch)
    end
end

local function OnCooldownOver(inst)
    inst.components.activatable.inactive = TUNING.SPAWN_EYEOFTERROR
    inst.AnimState:Show("terrarium_tree")
    inst.components.inventoryitem:ChangeImageName(nil) -- back to default
end

local function OnActivate(inst, doer)
    if not inst.is_on then
        TurnOn(inst)
    else
        TurnOff(inst)
    end
end

local function OnWitchLeft(witch, inst)
    if witch ~= nil and not witch:IsInLimbo() then
        witch:RemoveFromScene()
    end
end

local function OnPutInInventory(inst)
    TurnOff(inst)
end

local function OnDroppedFromInventory(inst)
    disable_dynshadow(inst)
end

local function TimerDone(inst, data)
    local timer = data ~= nil and data.name
    if timer == "tp_boss_to_door" then
        if inst.components.activatable.inactive then
            OnBossFightOver(inst)
            Witch_SpawnPrefabAroundDoor(inst.witch_prefab, 10, 20)
        end
    elseif timer == "summon_delay" then
        StartSummoning(inst)
    elseif timer == "warning" then
        SpawnWitch(inst)
    elseif timer == "cooldown" then
        OnCooldownOver(inst)
        resetTpTime(inst)
    end
end

local function GetActivateVerb(inst)
    return "TOUCH"
end

local function OnNight(inst)
    if CanSummon(inst) then
        if inst.is_on then
            inst.components.timer:StartTimer("summon_delay", TUNING.TERRARIUM_SUMMON_DELAY)
        end
    else
        if inst.components.timer:TimerExists("warning") then
            TheNet:Announce(STRINGS.EYEOFTERROR_CANCEL)
            TurnOff(inst)
        elseif inst.witch ~= nil then
            OnDay_SendBossAway(inst)
        end
    end
end

-------------------------------------------------------------------------------

local function OnSave(inst, data)
    data.is_on = inst.is_on

    local refs = nil
    if inst.witch ~= nil then
        -- If the boss is dying as we save, record it.
        if inst.witch.components.health then
            data.boss_dead = inst.witch.components.health:IsDead()
        end
        if inst.witch.persists then
            data.boss_guid = inst.witch.GUID
            refs = { inst.witch.GUID }
        end
    end

    return refs
end

local function OnLoadPostPass(inst, newents, data)
    if data ~= nil then
        if data.boss_guid then
            if newents[data.boss_guid] ~= nil then
                inst.witch = newents[data.boss_guid].entity

                hookup_witch_listeners(inst, inst.witch)

                if not CanSummon(inst) then
                    OnWitchLeft(inst.witch, inst)
                end
            end
        end

        if data.is_on then
            if (inst.witch ~= nil and not inst.witch:IsInLimbo())
                or inst.components.timer:TimerExists("warning") then
                TurnOn(inst, true)
                StartSummoning(inst, true)
            elseif data.boss_dead then
                -- The boss was dying as we saved, so we should be turned off as though
                -- we received the death message.
                OnBossFightOver(inst)
            elseif TUNING.SPAWN_EYEOFTERROR then
                TurnOn(inst, true)
            end
        elseif inst.components.worldsettingstimer:ActiveTimerExists("cooldown") then
            OnBossFightOver(inst)
        end
    end
end

-------------------------------------------------------------------------------

local function common()
    local inst = CreateEntity()

    inst.entity:AddTransform()
    inst.entity:AddAnimState()
    inst.entity:AddSoundEmitter()
    inst.entity:AddMiniMapEntity()
    inst.entity:AddLight()
    inst.entity:AddDynamicShadow()
    inst.entity:AddNetwork()

    inst.MiniMapEntity:SetIcon("terrarium.png")

    inst.Light:SetRadius(0)
    inst.Light:SetIntensity(0.45)
    inst.Light:SetFalloff(1.8)
    inst.Light:SetColour(1, 1, 1)
    inst.Light:Enable(true)
    inst.Light:EnableClientModulation(true)

    inst.DynamicShadow:SetSize(1.25, 1)
    inst.DynamicShadow:Enable(false)

    inst.AnimState:SetBank("terrarium")
    inst.AnimState:SetBuild("terrarium")
    inst.AnimState:PlayAnimation("idle")
    inst.AnimState:Hide("terrarium_tree_crimson")

    MakeInventoryPhysics(inst)

    inst:AddTag("irreplaceable")

    -- tags from trader.lua for optimization
    inst:AddTag("trader")
    inst:AddTag("alltrader")

    inst.GetActivateVerb = GetActivateVerb

    inst._LightTask = nil
    inst._lightframe = net_smallbyte(inst.GUID, "terrarium._lightframe", "lightdirty")
    inst._islighton = net_bool(inst.GUID, "terrarium._islighton", "lightdirty")
    inst._islighton:set(false)

    inst.entity:SetPristine()

    inst.scrapbook_hide = { "terrarium_tree_crimson" }

    if not TheWorld.ismastersim then
        inst:ListenForEvent("lightdirty", OnLightDirty)

        return inst
    end

    inst.on_end_witch_fn = function()
        if inst.witch ~= nil then
            OnBossFightOver(inst)
            inst.witch = nil
        end
    end

    inst.on_witch_left_fn = function(e)
        OnWitchLeft(e, inst)
    end

    inst:AddComponent("inspectable")
    inst.components.inspectable.nameoverride = "terrarium"

    inst:AddComponent("worldsettingstimer")

    inst:AddComponent("timer")
    inst:ListenForEvent("timerdone", TimerDone)

    inst:AddComponent("inventoryitem")
    inst.components.inventoryitem:SetOnPutInInventoryFn(OnPutInInventory)
    inst.components.inventoryitem:SetOnDroppedFn(OnDroppedFromInventory)
    inst.components.inventoryitem:SetSinks(true)
    inst.components.inventoryitem:ChangeImageName("terrarium")

    inst:AddComponent("hauntable")
    inst.components.hauntable:SetHauntValue(TUNING.HAUNT_TINY)

    inst:AddComponent("activatable")
    inst.components.activatable.OnActivate = OnActivate
    inst.components.activatable.quickaction = true

    inst.OnSave = OnSave
    inst.OnLoadPostPass = OnLoadPostPass

    inst:WatchWorldState("isnight", OnNight)

    return inst
end

local function walpurgisnight_boundary()
    local inst = common()

    if not TheWorld.ismastersim then
        return inst
    end

    inst.witch_prefab = "walpurgisnight"

    inst.cooldown_time = TUNING.WALPURGISNIGHT_BOUNDARY_COOLDOWN
    inst.components.worldsettingstimer:AddTimer("cooldown", inst.cooldown_time, true)

    inst.tp_boss_to_door_time = TUNING.WALPURGISNIGHT_BOUNDARY_TELEPORT_TIME
    inst.components.timer:StartTimer("tp_boss_to_door", inst.tp_boss_to_door_time)

    return inst
end

local function witch_gisela_boundary()
    local inst = common()

    if not TheWorld.ismastersim then
        return inst
    end

    inst.witch_prefab = "witch_gisela"

    inst.cooldown_time = TUNING.WITCH_GISELA_BOUNDARY_COOLDOWN
    inst.components.worldsettingstimer:AddTimer("cooldown", inst.cooldown_time, true)

    inst.tp_boss_to_door_time = TUNING.WITCH_GISELA_BOUNDARY_TELEPORT_TIME
    inst.components.timer:StartTimer("tp_boss_to_door", inst.tp_boss_to_door_time)

    return inst
end

local function witch_shadow_knight_boundary()
    local inst = common()

    if not TheWorld.ismastersim then
        return inst
    end

    inst.witch_prefab = "witch_shadow_knight"

    inst.cooldown_time = TUNING.SHADOW_KNIGHT_BOUNDARY_COOLDOWN
    inst.components.worldsettingstimer:AddTimer("cooldown", inst.cooldown_time, true)

    inst.tp_boss_to_door_time = TUNING.SHADOW_KNIGHT_BOUNDARY_TELEPORT_TIME
    inst.components.timer:StartTimer("tp_boss_to_door", inst.tp_boss_to_door_time)

    return inst
end

local function witch_deciduoustrees_boundary()
    local inst = common()

    if not TheWorld.ismastersim then
        return inst
    end

    inst.witch_prefab = "witch_deciduoustrees"

    inst.cooldown_time = TUNING.DECIDUOUSTREES_BOUNDARY_COOLDOWN
    inst.components.worldsettingstimer:AddTimer("cooldown", inst.cooldown_time, true)

    inst.tp_boss_to_door_time = TUNING.DECIDUOUSTREES_BOUNDARY_TELEPORT_TIME
    inst.components.timer:StartTimer("tp_boss_to_door", inst.tp_boss_to_door_time)

    return inst
end

return Prefab("walpurgisnight_boundary", walpurgisnight_boundary, assets),
    Prefab("witch_gisela_boundary", witch_gisela_boundary, assets),
    Prefab("witch_shadow_knight_boundary", witch_shadow_knight_boundary, assets),
    Prefab("witch_deciduoustrees_boundary", witch_deciduoustrees_boundary, assets)
